本节将详细的介绍了如何使用 Express 开发一个 Web 服务器。
包含从项目创建 => 编写代码&创建服务 => 测试服务 => 线上部署完整流程。
新建一个目录 express-demo
,进入目录,执行 npm init -y
初始化项目。
同时设置 package.json
的 type
字段为 module
,以便使用 ESM 语法编写代码 (当然也可以使用构建工具转换语法,这个后面单独介绍)。
① 安装依赖
sh
npm i express
② 创建 app.js
文件,编写如下内容
```js import express from 'express'
const PORT = 3000 // 用于设置端口号 const app = express() // 创建一个express应用程序实例
// 创建一个 GET /hello 路由 app.get('/hello', (req, res) => { // 返回一个包含 "Hello World" 的 H1 标题的响应 res.send('
// 启动 Express 应用程序,监听在指定的端口上
app.listen(PORT, () => {
// 在控制台输出服务器运行信息
console.log(Server is running at http://localhost:${PORT}
)
})
```
③ 安装 nodemon
,用于监听文件变化,自动重启服务
sh
npm i nodemon -D
④ 配置开发启动指令
package.json
加入如下内容。
json
{
"scripts": {
"dev": "nodemon app.js"
}
}
使用 npm run dev
启动项目。
打开浏览器访问 http://localhost:3000/hello
,可以看到如下页面。
注意:如果 localhost
无法访问,可能是 hosts
文件没有配置与 127.0.0.1
的映射,可以使用 127.0.0.1
访问(http://127.0.0.1:3000/hello
),或者配置 hosts 文件。
通常后端开发我们都会使用一些客户端工具来调试我们的接口,
这里推荐一个开源的 postcat,非常轻量,无需登录注册也可使用,能满足基本的开发调试。
当然功能更加丰富的可以使用 eolink 或者 Apifox,这里不再过多展开,读者选取一个趁手的就行。
下面使用 postcat
演示 API 测试。
安装好后,打开可看见如下界面,先配置一个本地环境。
在 API 页面发起请求的示例如下。
可以看到这里返回了我们设置的内容。
我们可以再修改一下代码为 (保存后会自动重启立马生效)
js
res.send('<h1>Hello Express</h1>')
接下来介绍一下 Express 相关方法的使用,过程中再进一步介绍 postcat
的使用。
用于设置中间件函数来处理请求和响应,Express 会按照设置的顺序依次调用中间件函数。
中间件函数支持接受三个参数:
写一个 app.use
放在之前的 app.get
之前。
``js
app.use((req, res, next) => {
const { method, path } = req
console.log(
[${method}] ${path}`)
next()
})
// 省略之前的代码 // app.get('/hello',xxx) ```
在通过服务请求可以查看所有请求的方法和路径被打印到终端里。
支持通过 query
,headers
,body
,params
传递参数。
我们继续完善一下刚刚的 app.use
代码。
js
app.use((req, res, next) => {
const { method, path, query, body, headers } = req
console.log(`[${method}] ${path}`)
console.log('query:', query)
console.log('headers:', headers)
console.log('body:', body)
next()
})
再用工具测试一下。
设置一些传递的参数,
发送请求,结果如下。
可以看到正确的获取了传递的 query
和 headers
但是 body 是空的。
这是因为 express
默认不支持解析传递的请求体数据,所以我们需要引入一个中间件来解析这种类型的数据,这里使用 express.json() 这个内置的中间件 (基于 body-parser)。
js
const app = express() // 创建一个express应用程序实例
// 支持JSON数据解析
app.use(express.json())
再次发送请求就可以看到获取到了 body
数据。
其中 params
主要指路由中携带的 REST 参数,下面是个示例。
js
app.get('/hello/:id', (req, res) => {
const { params } = req
console.log('params', params)
res.json(params)
})
请求 GET /hello/123
结果如下。
也可以利用工具提供的 REST
参数,请求路径调整为 GET /hello/{id}
(部分工具支持的格式也可能是 GET /hello/:id
)。
我们可以通过 app.get
、app.post
、app.put
、app.delete
等方法来设置不同的请求方法,格式如下。
js
app.METHOD(PATH, HANDLER)
下面我们单独新建一个文件 routes/method.js
来使用这些方法。
js
export default function mountMethodDemo(app) {
app.get('/method/get', (req, res) => {
res.send('GET request')
})
app.post('/method/post', (req, res) => {
res.send('POST request')
})
app.put('/method/put', (req, res) => {
res.send('PUT request')
})
app.delete('/method/delete', (req, res) => {
res.send('DELETE request')
})
}
在 app.js
中引入。
js
import mountMethodDemo from './routes/method.js'
// 省略之前的代码
mountMethodDemo(app)
然后就可以通过工具来测试了。
同时还支持一种特殊的 app.all
,可以匹配所有的请求方法。
js
app.all('/method/all', (req, res) => {
const { method } = req
res.send(`${method} request`)
})
Express 使用 path-to-regexp 来匹配路由路径,
路由路径可以是 字符串
、字符串模式
或 正则表达式
。
其中字符串的方式,就是上面的常规写法。
\===字符串模式的方式===
可以使用 ?(存在或者不存在)
、+(连续一个或多个)
、*(任意字符0个或多个)
和 ()
等特殊字符。
js
// 匹配 acd 和 abcd
app.get('/ab?cd', (req, res) => {
res.send('ab?cd')
})
// 匹配 abcd、abbcd、abbbcd等
app.get('/ab+cd', (req, res) => {
res.send('ab+cd')
})
// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等
app.get('/ab*cd', (req, res) => {
res.send('ab*cd')
})
// 匹配 /abe 和 /abcde
app.get('/ab(cd)?e', (req, res) => {
res.send('ab(cd)?e')
})
\===正则模式===
可以使用正则表达式来匹配路由。
js
// 匹配路径中含有 world 的路径
app.get(/world/, (req, res) => {
res.send('hello hello')
})
使用 express.Router
可以创建单独的路由实例,这样非常方便我们模块化开发应用。
Router
实例是完整的中间件和路由系统。
下面我们新建一个文件 routes/router-demo.js
来演示 Router
实例的应用。
```js import express from 'express'
const router = express.Router()
router.get('/router/get', (req, res) => { res.send('GET router request') })
router.post('/router/post', (req, res) => { res.send('POST router request') })
export default router ```
在 app.js
中引入。
```js import demoRouter from './routes/router-demo.js' // 省略其他代码
// 注册 demoRouter 路由 app.use(demoRouter) // 将 demoRouter 路由注册到 /demo 路径下,路由会自动拼接上 /demo 前缀 app.use('/demo', demoRouter) ```
请求结果如下。
app.route()
可以用来创建链式路由,可以避免重复的路由名称。
可以用于创建相同路由名称的不同请求方法,同时可以通过 all
设置所有请求的前置处理逻辑。
js
app
.route('/route/any')
.all((req, res, next) => {
console.log('pre all', req.method, req.path)
next()
})
.get((req, res) => {
console.log('get request')
res.send('get request')
})
.post((req, res) => {
console.log('post request')
res.send('post request')
})
测试结果如下。
express 内置了许多开箱即用的设置响应数据的方法,下面我们介绍一些常用的,
我们新建一个文件 routes/response.js
来存放这一小节的路由代码 (同理在 app.js
中引入即可。
```js // routes/response.js import express from 'express'
const router = express.Router() // 省略后续的路由代码
export default router
// app.js中引入 import responseRouter from './routes/response.js'
app.use(responseRouter) ```
① res.json
主要用于发送 JSON 数据。
js
router.get('/response/json', (req, res) => {
res.json({
name: 'express',
type: 'framework'
})
})
② res.send
可以用于发送任意类型的数据。
```js router.get('/response/send', (req, res) => { // html res.send('
// json // res.send({ // name: 'express', // type: 'framework' // })
// string // res.send('hello express')
// Buffer // res.send(Buffer.from('hello express')) }) ```
③ res.download
用于下载文件。
```js import path from 'path'
router.get('/response/download', (req, res) => { // 指定文件路径 // res.download('package.json') res.download(path.resolve('./package.json')) }) ```
打开浏览器访问 http://localhost:3000/response/download
即可看见触发了文件下载。
新建一个文件 routes/headers.js
来存放这一小节的路由代码 (同理在 app.js
中引入即可。
① 获取 Request header
直接通过 req.headers
即可获取到请求头。
js
router.get('/response/get/header', (req, res) => {
res.json(req.headers)
})
② 设置 Response header
可以通过 res.set
来设置 response header。
js
router.get('/response/set/header', (req, res) => {
res.set('Content-Type', 'text/html')
res.set('token', '123456')
res.send('<h1>hello express</h1>')
})
前面的代码中的路由有一部分是写在 app.js
中,导入注册也是在 app.js 中,这样会导致代码相对臃肿,我们可以处理成下面的目录结构。
sh
├── app.js
├── middleware
| └── index.js
├── package-lock.json
├── package.json
└── routes
├── headers.js
├── index.js
├── method.js
├── response.js
└── router-demo.js
将中间件注册和路由注册的逻辑放在 app.js
中,具体实现放在 routes/index.js
和 middleware/index.js
下。
这样 app.js 的代码就变成了这样,看上去就简洁很多了。
```js // app.js import express from 'express' import mountMiddleware from './middleware/index.js' import mountRouters from './routes/index.js'
const PORT = 3000 // 用于设置端口号 const app = express() // 创建一个express应用程序实例
mountMiddleware(app) mountRouters(app)
// 启动 Express 应用程序,监听在指定的端口上
app.listen(PORT, () => {
// 在控制台输出服务器运行信息
console.log(Server is running at http://localhost:${PORT}
)
})
```
mountMiddleware
函数的实现如下。
```js import express from 'express'
export default function mountMiddleware(app) { app.use(express.json()) // 支持body解析
// 自定义中间件函数
app.use((req, res, next) => {
const { method, path, query, body, headers } = req
console.log([${method}] ${path}
)
console.log('query:', query)
console.log('headers:', headers)
console.log('body:', body)
next()
})
}
```
mountRouters
函数的实现如下。
```js import headerRouter from './headers.js' import responseRouter from './response.js' import demoRouter from './router-demo.js' import mountMethodDemo from './method.js'
const routers = [headerRouter, responseRouter, demoRouter]
export default function mountRouters(app) { mountMethodDemo(app)
// 注册所有router app.use(routers)
// 将 demoRouter 路由注册到 /demo 路径下,路由会自动拼接上 /demo 前缀 app.use('/demo', demoRouter)
// 一些自定义路由 app.get('/hello/:id', (req, res) => { const { params } = req console.log('params', params) res.json(params) })
// 创建一个 GET /hello 路由 app.get('/hello', (req, res) => { // 返回一个包含 "Hello World" 的 H1 标题的响应 // res.send('
上面只介绍了一些 express 入门掌握的内容,更多 API 可以阅读官方文档掌握:
在上一章中我们定义了一些 Restful
风格的 API,我们这里可以利用前面掌握的内容来简单实现一下。
新建 routes/restful.js
文件,编写一下如下路由。
| 方法 | 路径 | 描述 | | ------ | -------------- | -------------------------- | | GET | /api/users | 获取所有用户信息 | | GET | /api/users/:id | 根据用户ID获取用户信息 | | POST | /api/users | 创建新用户,请求体包含新用户的信息 | | PUT | /api/users/:id | 根据用户ID更新用户信息,请求体包含更新后的用户信息 | | DELETE | /api/users/:id | 根据用户ID删除用户信息 |
```js import express from 'express'
const router = express.Router()
// 用于测试的数据 const userList = [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' } ]
router.get('/users', (req, res) => { res.json(userList) })
router.get('/users/:id', (req, res) => { // 根据用户 id 查找用户信息 const user = userList.find((item) => item.id === Number(req.params.id)) res.json(user) })
router.post('/users', (req, res) => { // 创建新用户 const user = { id: userList.length + 1, name: req.body.name // 从请求体中获取用户名 } userList.push(user) res.json(user) })
router.put('/users/:id', (req, res) => { // 根据用户 id 查找用户信息 const user = userList.find((item) => item.id === Number(req.params.id)) // 更新用户名称 user.name = req.body.name // 从请求体中获取新的用户名 res.json(user) })
router.delete('/users/:id', (req, res) => { // 查找要删除的用户在列表中的索引位置 const index = userList.findIndex((item) => item.id === Number(req.params.id)) // 获取要删除的用户信息 const delUser = userList[index] // 从列表中删除该用户 userList.splice(index, 1) res.json({ message: '删除成功', data: delUser }) })
export default router ```
然后在 routes/index.js
中注册路由。
```js import restfulRouter from './restful.js'
app.use('/api', restfulRouter) ```
测试结果如下。
| 方法 | 路径 | 结果 | | ------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | /api/users | | | GET | /api/users/:id | | | POST | /api/users | | | PUT | /api/users/:id | | | DELETE | /api/users/:id | |
可以使用 express.static
设置静态资源目录,
这样可以直接访问目标目录下的文件资源。
js
app.use(express.static('public'))
访问 http://localhost:3000/
即可看到效果。
几个文件代码如下。
```html
```
css
h1{
color: red;
}
js
window.onload = () => {
document.querySelector('#btn').addEventListener('click', () => {
window.alert('Hello World!')
})
}
处理文件上传的第三方库有很多,比如 busboy,multer,formidable 等等。
这里我们使用 multer
来处理文件上传。
① 安装依赖
sh
npm i multer
② 编写文件上传路由 routes/upload.js
js
// 引入需要的模块
import express from 'express' // 引入 Express 框架
import multer from 'multer' // 引入 Multer 模块
import fs from 'fs'
// 引入 Node.js 文件系统模块
const router = express.Router()
// 指定文件存储位置和文件名
const storage = multer.diskStorage({
destination(req, file, cb) {
// 这里的 destination() 函数指定了文件存储的目录
const dir = './uploads' // './uploads' 为指定文件存储的目录
if (!fs.existsSync(dir)) {
// 如果该目录不存在,则创建该目录
fs.mkdirSync(dir, { recursive: true })
}
cb(null, './uploads') // 将文件存储到指定目录
},
filename(req, file, cb) {
// 这里的 filename() 函数指定了文件命名规则
const ext = file.originalname.split('.').pop() // 获取文件后缀名
cb(null, `${Date.now()}-${file.fieldname}.${ext}`) // 将文件存储到指定位置,并以指定的文件名命名
}
})
// 创建一个 multer 实例并配置相关选项
const upload = multer({
storage, // 存储位置和文件名规则
limits: {
fileSize: 1024 * 1024 * 5 // 限制文件大小为 5 MB
},
fileFilter(req, file, cb) {
// 这里的 fileFilter() 函数指定了文件类型过滤规则
// 拒绝上传非图片类型的文件
if (!file.mimetype.startsWith('image/')) {
const err = new Error('Only image files are allowed!') // 错误的具体信息
err.status = 400 // 设置错误状态码为 400
return cb(err, false)
}
return cb(null, true)
}
})
// 处理文件上传请求
router.post('/upload/image', upload.single('file'), (req, res) => {
// 这里的 upload.single() 函数指定了只上传单个文件
res.json({ message: '文件上传成功', data: req.file }) // 返回上传成功的信息和上传的文件信息
})
export default router // 导出路由模块
③ 新建 public/upload.html
上传文件页面
```html
```
④ 访问页面测试 http://localhost:3000/upload.html
测试页面上传。
上传结果如下。
要让应用真正的活起来,当然离不开数据库,常用的数据库有 MySQL、MongoDB、Redis
等等。
PS:默认读者已经了解了数据库的基本操作,本小节不会介绍数据库相关的基本知识和安装
关于SQL入门大家可以阅读 廖雪峰: SQL教程
数据库管理工具,笔者使用 VS Code 插件: Database Client,支持操作常见的关系型与非关系型数据库
在配置
localhost
不生效的场景,可将其改为127.0.0.1
下面介绍一下如何在 Node.js 中操作数据库。
常用的操作数据库的第三方库有很多,比如 Sequelize、Mongoose,typeorm,prisma 等等。
下面是一个近期的下载量对比。
Sequelize
,Mongoose
略多一点,后 2 者比较适合大型项目使用,功能更加丰富。
下面分别介绍一下使用它们如何简单操作 mysql 和 mongodb。
新建一个 node_test
数据库和一张用于测试的 users
表。
```sql create database node_test;
CREATE TABLE IF NOT EXISTS users
(
id
INT(11) NOT NULL AUTO_INCREMENT,
name
VARCHAR(50) NOT NULL,
age
INT(3) NOT NULL,
PRIMARY KEY (id
)
);
```
创建 db/mysql.js
文件存放本小节代码。
安装依赖。
sh
npm i sequelize mysql2
创建 Sequelize
实例。
```js import Sequelize from 'sequelize'
const sequelize = new Sequelize('node_test', 'root', 'password', { host: 'localhost', dialect: 'mysql' }) ```
定义 users
表对应的模型。
js
// 定义模型
const User = sequelize.define(
'User',
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
name: {
type: Sequelize.STRING(50),
allowNull: false
},
age: {
type: Sequelize.INTEGER(3),
allowNull: false
}
},
{
tableName: 'users', // 指定表格名称
timestamps: false // 禁止 Sequelize 自动生成 createdAt 和 updatedAt 字段
}
)
创建 CRUD
操作方法。
```js // 创建记录 async function createUser(name, age) { const user = await User.create({ name, age }) return user.toJSON() }
// 查询所有记录 async function findAllUsers() { const users = await User.findAll() return users.map((user) => user.toJSON()) }
// 根据 id 查询记录 async function findUserById(id) { const user = await User.findByPk(id) return user?.toJSON() }
// 更新记录 async function updateUser(id, name, age) { const user = await User.findByPk(id) if (user) { user.name = name user.age = age await user.save() console.log(user.toJSON()) } else { console.log('User not found') } return user }
// 删除记录 async function deleteUser(id) { const user = await User.findByPk(id) if (user) { await user.destroy() console.log('User deleted') } else { console.log('User not found') } return user }
export const UserDb = { User, createUser, findAllUsers, findUserById, updateUser, deleteUser } ```
创建一个测试文件 tests/mysql.js
测试上述的方法。
```js import { UserDb } from '../db/mysql.js'
// 测试 async function test() { // 添加3个用户信息 console.log('create', await UserDb.createUser('Alice', 25)) console.log('create', await UserDb.createUser('Tom', 20)) console.log('create', await UserDb.createUser('Lisa', 22))
// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers())
// 查询id为1的用户信息 console.log('findById', await UserDb.findUserById(1))
// 更新id为1的用户信息 await UserDb.updateUser(1, 'Alice Smith', 28) // 删除id为2的用户信息 await UserDb.deleteUser(2)
// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers()) }
test() ```
| 运行前 | 运行结果 | 运行后 | | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | | | |
开发中只需要将数据库操作和路由结合就能完成常规的 CRUD
接口开发了。
① 项目安装依赖
sh
npm i mongoose
创建一个 db/mongodb.js
存放本小节代码
② 建立数据库连接
js
// 引入mongoose库
import mongoose from 'mongoose'
// 连接MongoDB数据库,本地地址为mongodb://localhost:27017/node_test
mongoose.connect('mongodb://localhost:27017/node_test')
③ 定义模型
```js // 获取Schema对象 const { Schema } = mongoose
// 定义userSchema,包含id、name和age字段 const userSchema = new Schema({ id: Number, name: String, age: Number }) // 根据userSchema创建用户模型,集合名为users const User = mongoose.model('User', userSchema, 'users') ```
④ 创建 CRUD
操作方法
```js // 创建记录 function createUser(id, name, age) { const newUser = new User({ id, name, age }) // 保存记录并返回Promise实例 return newUser.save() }
// 查询所有记录 function findAllUsers() { // 查找所有用户记录,并返回Promise实例 return User.find({}) } // 根据id查询记录 function findUserById(id) { // 根据id查找用户记录,并返回Promise实例 return User.findOne({ id }) }
// 更新记录 function updateUser(id, name, age) { // 根据id更新用户记录,并返回Promise实例 return User.updateOne({ id }, { name, age }) }
// 删除记录 function deleteUser(id) { // 根据id删除用户记录,并返回Promise实例 return User.deleteOne({ id }) }
// 导出定义的库对象,包括User模型和各种操作方法 export const UserDb = { User, createUser, findAllUsers, findUserById, updateUser, deleteUser } ```
编写一个测试文件 tests/mongodb.js
测试上述的方法。
```js import { UserDb } from '../db/mongodb.js'
// 测试 async function test() { // 添加3个用户信息 console.log('create', await UserDb.createUser(1, 'Alice', 25)) console.log('create', await UserDb.createUser(2, 'Tom', 20)) console.log('create', await UserDb.createUser(3, 'Lisa', 22))
// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers())
// 查询id为1的用户信息 console.log('findById', await UserDb.findUserById(1))
// 更新id为1的用户信息 await UserDb.updateUser(1, 'Alice Smith', 28)
// 删除id为2的用户信息 await UserDb.deleteUser(2)
// 查询所有的用户信息 console.log('findAll', await UserDb.findAllUsers()) } test() ```
| 运行前 | 运行结果 | 运行后 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | | | | |
针对 id mongodb 会自动生成一个私有的字段 _id
,用户也可以使用 MongoDB-ObjectId 去生成自己的字段。
下面是 ObjectId 的使用示例。
```js // 引入mongoose库 import mongoose from 'mongoose' // 获取ObjectId对象 const { ObjectId } = mongoose.Types // 创建新的objectId实例 const objectId = new ObjectId()
// 打印objectId实例 console.log(objectId) // 打印objectId实例的字符串格式 console.log(objectId.toString()) // 打印objectId实例的16进制字符串格式 console.log(objectId.toHexString()) // 打印objectId实例的日期时间戳 console.log(objectId.getTimestamp()) ```
开发的时候我们使用了 nodemon
部署的时候,通常需要考虑 日志记录
,崩溃自动重启
,应用监控
等问题,
因此我们线上部署就要使用一些工具辅助部署解决这些问题。
常用的部署方式有两种,一种是直接部署到服务器 (如使用 PM2),另一种是使用容器部署 (如 Docker)。
我们主要介绍一下 PM2
的用法。
PM2 是一个守护进程管理器,可以帮助我们管理和监控 Node.js 应用。
通过 npm
即可一键安装。
sh
npm i -g pm2
启动方式有很多种,咱们一个个介绍一下。
① 类似 Node 直接执行的方式
sh
pm2 start app.js
查看运行日志。
sh
pm2 logs
此时我们访问 http://localhost:3000
即可看到之前设置的页面。
修改文件服务也不会 重启
。
可以使用 pm2 stop <name>
来停止目标服务。
sh
pm2 stop app
② 启动时设置服务名称
sh
pm2 start app.js --name myapp
运行结果。
③ 通过运行指定 npm 指令启动
通常我们会在项目里定义一个 npm start script
用于启动项目。
json
"scripts": {
"dev": "nodemon app.js",
"start": "node app.js"
}
可以通过 pm2 start npm
来启动项目。
```sh
pm2 start npm --name app2 -- run start ```
进一步了解可以阅读文章为什么 Node 应用要用 PM2 来跑?。
本节详细的介绍了如何使用 Express 开发一个 Web 服务器。
(从项目创建 => 编写代码&创建服务 => 测试服务 => 线上部署)
过程中介绍了 Express
常见用法和一些重要概念:
中间件
,路由
等概念;实践部分介绍了:
express.static
设置静态资源目录;multer
处理文件上传;最后详细介绍了如何使用 PM2
对项目进行部署,介绍了多种部署方式。